package com.lin.nio.channel.multiplexing;

import com.lin.nio.utils.ByteBufferUtil;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

/**
 * #### 多路复用（也是同步的）
 * 单线程可以配合 Selector 完成对多个 Channel 可读写事件的监控，这称之为多路复用
 * <p>
 * * 多路复用仅针对网络 IO、普通文件 IO 没法利用多路复用
 * * 如果不用 Selector 的非阻塞模式，线程大部分时间都在做无用功，而 Selector 能够保证
 * * 有可连接事件时才去连接
 * * 有可读事件才去读取
 * * 有可写事件才去写入
 * * 限于网络传输能力，Channel 未必时时可写，一旦 Channel 可写，会触发 Selector 的可写事件
 *
 * 缺点：由于是用一个线程配合selector完成对多个channel的连接、可读、可写事件的处理，如果某个事件
 * 处理事件太久，也会影响整体的效率。
 *  --使用多线程
 *  BOSS线程：接收所有的客户端连接，创建子线程来处理客户端的读写事件。
 *  注意创建的线程
 */
public class MultiplexServer {
    public static void main(String[] args) throws IOException {
        // 1创建一个selector用于管理多个channel
        Selector selector = Selector.open();

        // 创建一个服务端
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        ssc.bind(new InetSocketAddress(1123));

        /**
         * SelectionKey代表channel和selector的注册
         */
        SelectionKey sscKey = ssc.register(selector, 0, null);// 为了ssc能被selector管理，将ssc注册到selector中
        sscKey.interestOps(SelectionKey.OP_ACCEPT); // ssc只关注连接事件

        // 2循环处理事件
        while (true) {
            System.out.println("连接前");
            selector.select(); // 这个方法会阻塞，只有当selector选择了一个通道(selector管理的通道有事件触发)或其他情况，select方法才会返回

            Set<SelectionKey> selectionKeys = selector.selectedKeys(); // 获取和selector选中的key集合
            Iterator<SelectionKey> iter = selectionKeys.iterator();
            while (iter.hasNext()) {
                SelectionKey key = iter.next();
                SelectableChannel channel = key.channel();// 返回创建该key的channel
                // 3判断事件类型
                if (key.isAcceptable()) { // 可连接事件
                    SocketChannel sc = ((ServerSocketChannel) channel).accept();
                    sc.configureBlocking(false);
                    SelectionKey scKey = sc.register(selector, 0, null);
                    scKey.interestOps(SelectionKey.OP_READ);

                    // 给scKey绑定一个附件
                    ByteBuffer buffer = ByteBuffer.allocate(16); // 给channel绑定一个buffer来缓存数据
                    scKey.attach(buffer);


                } else if (key.isReadable()) { // 可读事件
                    try {
                        /**
                         * 这里可能会发生数据的边界问题
                         * 就是服务器这边定义了16个字节来接收数据，可是来的数据可能大于16个字节：这样会导致触发两次可读事件，然后数据就会不连贯
                         *
                         * * 一种思路是固定消息长度，数据包大小一样，服务器按预定长度读取，缺点是浪费带宽（不够长度的用0补充）
                         * * 另一种思路是按分隔符拆分，缺点是效率低
                         * * TLV 格式，即 Type 类型、Length 长度、Value 数据，类型和长度已知的情况下，就可以方便获取消息大小，分配合适的 buffer，缺点是 buffer 需要提前分配，如果内容过大，则影响 server 吞吐量
                         *   * Http 1.1 是 TLV 格式
                         *   * Http 2.0 是 LTV 格式
                         *
                         * 解决方案其实就是要客户端和服务端之间遵循同一种协议来收发数据。
                         */
                        ByteBuffer dst = (ByteBuffer) key.attachment();
                        SocketChannel sc = (SocketChannel) channel;
                        sc.read(dst); // 客户端断开连接会触发读事件
                        if (dst.position() == dst.limit()) { // 需要扩容
                            dst.flip();
                            ByteBuffer newBuffer = ByteBuffer.allocate(dst.capacity() * 2);
                            newBuffer.put(dst);
                            key.attach(newBuffer);
                        } else
                            System.out.println("收到的数据：" + ByteBufferUtil.debugAll(dst));


                    } catch (IOException e) {
                        System.out.println("远程主机强迫关闭了一个现有的连接");
                        key.cancel(); // 取消channel和selector的注册
                    }
                }

                // 移除selectedKeys中处理好的key，否则有些情况key会重复处理(一个客户端发过来请求，然后发过来数据，请求会重复处理)
                iter.remove();
            }
        }

    }
}
